Skip to content

fix: measure Cleanup's size estimates the way the cleanup actually works - #2103

Merged
laurentiu021 merged 1 commit into
mainfrom
fix/cleanup-prescan-safe-walker
Sep 4, 2026
Merged

fix: measure Cleanup's size estimates the way the cleanup actually works#2103
laurentiu021 merged 1 commit into
mainfrom
fix/cleanup-prescan-safe-walker

Conversation

@laurentiu021

@laurentiu021 laurentiu021 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Closes #2098.

What was wrong

CleanupPreScanService sized both temp folders and the Recycle Bin with Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories). Three defects from one call:

  1. Silent under-reporting. AllDirectories throws UnauthorizedAccessException out of MoveNext(), and the catch has to sit outside the foreach — so the first folder Windows refuses ends the whole walk and the headline reports the partial sum, with nothing on screen to say so.
  2. Follows junctions. Deleting a folder shortcut puts a reparse point in the Recycle Bin, so one deleted junction could add every file it pointed at — live data — to the "in Recycle Bin" total.
  3. Counted what the sweep skips. Since [Bug]: Quick Cleanup - The temp sweep can delete the .NET extraction root of every running single-file app, and overcounts what it freed #2094 the temp cleanup excludes SystemPaths.BundleExtractionRoot and SystemPaths.OwnExtractionDirectory. The estimate did not, so it promised space Clean TEMP correctly refuses to free.

Both walks now go through TuneUpService.EnumerateFilesSkippingReparsePoints with the same two exclusions. That walker already carries tests for the directory-symlink, reparse-point-root and both-exclusion cases, so the pre-scan inherits proven behaviour rather than a second implementation.

The guard could not see this caller

EveryTempTreeWalkerCall_PassesBothExtractionExclusions checked walker arguments in a hardcoded ["TuneUpService.cs", "DeepCleanupService.cs"] list — the same weakness that let a third temp sweeper hide in CleanupViewModel for months. Two changes:

  • It finds callers by looking. The file list is now the two sanctioned sweepers plus any file containing a SkippingReparsePoints( call. Only TuneUpService's walkers are reachable from outside, so that is exact.
  • The declaration filter was matching call sites. It skipped any argument list containing CancellationToken — which CancellationToken.None contains, so both new calls would have been skipped as declarations and the guard would have read them and checked nothing. It now requires the type followed by a parameter name.

Vacuity floor re-measured 5 → 7 (five calls in the two sweepers, two in the pre-scan). Failure messages now name the file and the offending call.

Verification

Mutation proof, three cases, each restored byte-for-byte and re-hashed:

Mutation Result
a CleanupPreScanService call drops both exclusions REDthis walker call passes neither or only one extraction exclusion — CleanupPreScanService.cs: EnumerateFilesSkippingReparsePoints(...)
a DeepCleanupService call drops both exclusions RED — names DeepCleanupService.cs: EnumerateFiles(p, ct), so the loose-name coverage is intact
one call reverts to Directory.EnumerateFiles(AllDirectories) REDonly 6 walker calls were matched across 3 file(s), so the floor catches a regression to the unsafe API

Green at baseline and after restore. Regression sweep: ArchitectureTests 88 green (plus the harness-only author-header case for the throwaway runner), CleanupViewModelTests 104 green, TuneUpServiceTests 40 green. Builds 0 errors / 0 warnings, dotnet format --verify-no-changes clean on both projects, version consistency csproj 1.76.10 = CHANGELOG 1.76.10 = SECURITY 1.76.x.

Not covered by a test: the abort-on-protected-folder behaviour itself. Reproducing it needs a directory the test host cannot read, which on an elevated CI runner an admin bypasses anyway — that would be a machine-dependent test, not a deterministic one. The mechanical pin is mutation 3: the population floor reddens if any call site goes back to AllDirectories.

Docs

ICleanupPreScanService's contract offered "Unable to scan" as a possible temp label, which MeasureTemp cannot produce — it skips per path and always reaches Describe. Corrected to say what it really returns, including that an unreadable folder therefore reads as "Empty".

CleanupPreScanService sized both temp folders and the Recycle Bin with
SearchOption.AllDirectories, which is wrong three ways:

1. It throws UnauthorizedAccessException out of MoveNext(), and the catch has to
   sit outside the foreach -- so one protected subfolder ended the whole walk and
   the headline reported whatever had been summed up to that point, silently low.
2. It follows reparse points. Deleting a junction puts one IN the Recycle Bin, so
   a single deleted junction could add every file it pointed at to the bin total.
3. It counted the extraction roots that the temp sweep has skipped since #2094,
   promising space Clean TEMP would correctly refuse to free.

Both walks now go through TuneUpService.EnumerateFilesSkippingReparsePoints with
the same two exclusions, which already has tests for the symlink, reparse-root and
exclusion cases.

The guard for that call shape read a hardcoded two-file list, so it could not see
this caller at all -- the same weakness that let a third temp sweeper hide in a
view-model for months. It now finds callers by looking for them. Its declaration
filter matched the bare type name, which a call passing CancellationToken.None
contains too, so both new calls would have been skipped as declarations; it now
requires the type followed by a parameter name. Vacuity floor re-measured 5 -> 7.

ICleanupPreScanService's contract offered "Unable to scan" as a temp label the
method cannot produce; corrected to describe what it really returns.

Closes #2098
@laurentiu021
laurentiu021 merged commit cf63347 into main Sep 4, 2026
5 checks passed
@laurentiu021
laurentiu021 deleted the fix/cleanup-prescan-safe-walker branch September 4, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Cleanup — the "can be freed" figure stops counting at the first protected folder and follows junctions

1 participant